前言

上一篇文章中,我们讨论了 PHP自动加载功能PHP命名空间PSR0/PSR4标准,有了这些知识,其实我们就可以按照 PSR4标准 写出可以自动加载的程序了。然而我们为什么要自己写呢?尤其是有 Composer 这神一样的包管理器的情况下?

Composer自动加载概论

简介

Composer 是 PHP 的一个依赖管理工具。它允许你申明项目所依赖的代码库,它会在你的项目中为你安装他们。详细内容可以查看Composer 中文网

Composer Composer 将这样为你解决问题:

  • 你有一个项目依赖于若干个库。
  • 其中一些库依赖于其他库。
  • 你声明你所依赖的东西。
  • Composer 会找出哪个版本的包需要安装,并安装它们(将它们下载到你的项目中)。

例如,你正在创建一个项目,你需要一个库来做日志记录。你决定使用 monolog 。为了将它添加到你的项目中,你所需要做的就是创建一个 composer.json 文件,其中描述了项目的依赖关系。

  1. {
  2. "require": {
  3. "monolog/monolog": "1.2.*"
  4. }
  5. }

然后我们只要在项目里面直接use Monolog\Logger即可,神奇吧!

简单的说,Composer 帮助我们下载好了符合 PSR0/PSR4标准 的第三方库,并把文件放在相应位置;帮我们写了 __autoload() 函数,注册到了 spl_register() 函数,当我们想用第三方库的时候直接使用命名空间即可。

那么当我们想要写自己的命名空间的时候,该怎么办呢?很简单,我们只要按照 PSR4标准 命名我们的命名空间,放置我们的文件,然后在 composer 里面写好顶级域名与具体目录的映射,就可以享用 composer 的便利了。

当然如果有一个非常棒的框架,我们会惊喜地发现,在 composer 里面写顶级域名映射这事我们也不用做了,框架已经帮我们写好了顶级域名映射了,我们只需要在框架里面新建文件,在新建的文件中写好命名空间,就可以在任何地方 use 我们的命名空间了。

下面我们就以 Laravel 框架为例,讲一讲 composer 是如何实现 PSR0/PSR4标准 的自动加载功能。

Composer自动加载文件

  首先,我们先大致了解一下Composer自动加载所用到的源文件。

  1. autoload_real.php: 自动加载功能的引导类。
    • 任务是composer加载类的初始化(顶级命名空间与文件路径映射初始化)和注册(spl_autoload_register())。
  2. ClassLoader.php: composer加载类。
    • composer自动加载功能的核心类。
  3. autoload_static.php: 顶级命名空间初始化类,
    • 用于给核心类初始化顶级命名空间。
  4. autoload_classmap.php: 自动加载的最简单形式,
    • 有完整的命名空间和文件目录的映射;
  5. autoload_files.php: 用于加载全局函数的文件,
    • 存放各个全局函数所在的文件路径名;
  6. autoload_namespaces.php: 符合PSR0标准的自动加载文件,
    • 存放着顶级命名空间与文件的映射;
  7. autoload_psr4.php: 符合PSR4标准的自动加载文件,
    • 存放着顶级命名空间与文件的映射;

laravel框架下Composer的自动加载源码分析——启动


   laravel框架的初始化是需要composer自动加载协助的,所以laravel的入口文件index.php第一句就是利用composer来实现自动加载功能。

  1. <?php
  2. require __DIR__.'/../bootstrap/autoload.php';

咱们接着去看 bootstrap 目录下的 autoload.php

  1. <?php
  2. define('LARAVEL_START', microtime(true));
  3. require __DIR__ . '/../vendor/autoload.php';

再去 vendor 目录下的 autoload.php

  1. <?php
  2. require_once __DIR__ . '/composer' . '/autoload_real.php';
  3. return ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82e::getLoader();

为什么框架要在 bootstrap/autoload.php 转一下?个人理解,laravel 这样设计有利于支持或扩展任意有自动加载的第三方库。

好了,我们终于要看到了Composer真正要显威的地方了。autoload_real.php里面就是一个自动加载功能的引导类,这个类不负责具体功能逻辑,只做了两件事:初始化自动加载类、注册自动加载类。

到autoload_real这个文件里面去看,发现这个引导类的名字叫ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82e,为什么要叫这么古怪的名字呢?因为这是防止用户自定义类名跟这个类重复冲突了,所以在类名上加了一个hash值。其实还有一个做法我们更加熟悉,那就是不直接定义类名,而是定义一个命名空间。这里为什么不定义一个命名空间呢?个人理解:命名空间一般都是为了复用,而这个类只需要运行一次即可,以后也不会用得到,用hash值更加合适。

laravel框架下Composer的自动加载源码分析——autoload_real引导类


在 vendor 目录下的 autoload.php 文件中我们可以看出,程序主要调用了引导类的静态方法 getLoader() ,我们接着看看这个函数。

  1. <?php
  2. public static function getLoader()
  3. {
  4. /***************************经典单例模式********************/
  5. if (null !== self::$loader) {
  6. return self::$loader;
  7. }
  8. /***********************获得自动加载核心类对象********************/
  9. spl_autoload_register(
  10. array('ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82e', 'loadClassLoader'), true, true
  11. );
  12. self::$loader = $loader = new \Composer\Autoload\ClassLoader();
  13. spl_autoload_unregister(
  14. array('ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82e', 'loadClassLoader')
  15. );
  16. /***********************初始化自动加载核心类对象********************/
  17. $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION');
  18. if ($useStaticLoader) {
  19. require_once __DIR__ . '/autoload_static.php';
  20. call_user_func(
  21. \Composer\Autoload\ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::getInitializer($loader)
  22. );
  23. } else {
  24. $map = require __DIR__ . '/autoload_namespaces.php';
  25. foreach ($map as $namespace => $path) {
  26. $loader->set($namespace, $path);
  27. }
  28. $map = require __DIR__ . '/autoload_psr4.php';
  29. foreach ($map as $namespace => $path) {
  30. $loader->setPsr4($namespace, $path);
  31. }
  32. $classMap = require __DIR__ . '/autoload_classmap.php';
  33. if ($classMap) {
  34. $loader->addClassMap($classMap);
  35. }
  36. }
  37. /***********************注册自动加载核心类对象********************/
  38. $loader->register(true);
  39. /***********************自动加载全局函数********************/
  40. if ($useStaticLoader) {
  41. $includeFiles = Composer\Autoload\ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$files;
  42. } else {
  43. $includeFiles = require __DIR__ . '/autoload_files.php';
  44. }
  45. foreach ($includeFiles as $fileIdentifier => $file) {
  46. composerRequire832ea71bfb9a4128da8660baedaac82e($fileIdentifier, $file);
  47. }
  48. return $loader;
  49. }

  从上面可以看出,我把自动加载引导类分为5个部分。

第一部分——单例

第一部分很简单,就是个最经典的单例模式,自动加载类只能有一个。

  1. <?php
  2. if (null !== self::$loader) {
  3. return self::$loader;
  4. }

第二部分——构造ClassLoader核心类

第二部分 new 一个自动加载的核心类对象。

  1. <?php
  2. /***********************获得自动加载核心类对象********************/
  3. spl_autoload_register(
  4. array('ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82e', 'loadClassLoader'), true, true
  5. );
  6. self::$loader = $loader = new \Composer\Autoload\ClassLoader();
  7. spl_autoload_unregister(
  8. array('ComposerAutoloaderInit832ea71bfb9a4128da8660baedaac82e', 'loadClassLoader')
  9. );

loadClassLoader()函数:

  1. <?php
  2. public static function loadClassLoader($class)
  3. {
  4. if ('Composer\Autoload\ClassLoader' === $class) {
  5. require __DIR__ . '/ClassLoader.php';
  6. }
  7. }

从程序里面我们可以看出,composer 先向 PHP 自动加载机制注册了一个函数,这个函数 require 了 ClassLoader 文件。成功 new 出该文件中核心类 ClassLoader() 后,又销毁了该函数。

为什么不直接require,而要这么麻烦?原因就是怕有的用户也定义了个 \Composer\Autoload\ClassLoader 命名空间,导致自动加载错误文件。那为什么不跟引导类一样用个 hash 呢?因为这个类是可以复用的,框架允许用户使用这个类。

第三部分 —— 初始化核心类对象

  1. <?php
  2. /***********************初始化自动加载核心类对象********************/
  3. $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION');
  4. if ($useStaticLoader) {
  5. require_once __DIR__ . '/autoload_static.php';
  6. call_user_func(
  7. \Composer\Autoload\ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::getInitializer($loader)
  8. );
  9. } else {
  10. $map = require __DIR__ . '/autoload_namespaces.php';
  11. foreach ($map as $namespace => $path) {
  12. $loader->set($namespace, $path);
  13. }
  14. $map = require __DIR__ . '/autoload_psr4.php';
  15. foreach ($map as $namespace => $path) {
  16. $loader->setPsr4($namespace, $path);
  17. }
  18. $classMap = require __DIR__ . '/autoload_classmap.php';
  19. if ($classMap) {
  20. $loader->addClassMap($classMap);
  21. }
  22. }

这一部分就是对自动加载类的初始化,主要是给自动加载核心类初始化顶级命名空间映射。

初始化的方法有两种:

  1. 1. 使用autoload_static进行静态初始化;
  2. 2. 调用核心类接口初始化。

autoload_static静态初始化

静态初始化只支持 PHP5.6 以上版本并且不支持 HHVM 虚拟机。我们深入 autoload_static.php 这个文件发现这个文件定义了一个用于静态初始化的类,名字叫 ComposerStaticInit832ea71bfb9a4128da8660baedaac82e,仍然为了避免冲突加了 hash 值。这个类很简单:

  1. <?php
  2. class ComposerStaticInit832ea71bfb9a4128da8660baedaac82e{
  3. public static $files = array(...);
  4. public static $prefixLengthsPsr4 = array(...);
  5. public static $prefixDirsPsr4 = array(...);
  6. public static $prefixesPsr0 = array(...);
  7. public static $classMap = array (...);
  8. public static function getInitializer(ClassLoader $loader)
  9. {
  10. return \Closure::bind(function () use ($loader) {
  11. $loader->prefixLengthsPsr4
  12. = ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$prefixLengthsPsr4;
  13. $loader->prefixDirsPsr4
  14. = ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$prefixDirsPsr4;
  15. $loader->prefixesPsr0
  16. = ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$prefixesPsr0;
  17. $loader->classMap
  18. = ComposerStaticInit832ea71bfb9a4128da8660baedaac82e::$classMap;
  19. }, null, ClassLoader::class);
  20. }

这个静态初始化类的核心就是 getInitializer() 函数,它将自己类中的顶级命名空间映射给了 ClassLoader类。值得注意的是这个函数返回的是一个匿名函数,为什么呢?原因就是 ClassLoader类 中的 prefixLengthsPsr4prefixDirsPsr4等等方法都是private的。。。普通的函数没办法给类的 private 成员变量赋值。利用匿名函数的绑定功能就可以将把匿名函数转为 ClassLoader类 的成员函数。

关于匿名函数的绑定功能

接下来就是顶级命名空间初始化的关键了。

最简单的 classMap:

  1. <?php
  2. public static $classMap = array (
  3. 'App\\Console\\Kernel'
  4. => __DIR__ . '/../..' . '/app/Console/Kernel.php',
  5. 'App\\Exceptions\\Handler'
  6. => __DIR__ . '/../..' . '/app/Exceptions/Handler.php',
  7. 'App\\Http\\Controllers\\Auth\\ForgotPasswordController'
  8. => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/ForgotPasswordController.php',
  9. 'App\\Http\\Controllers\\Auth\\LoginController'
  10. => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/LoginController.php',
  11. 'App\\Http\\Controllers\\Auth\\RegisterController'
  12. => __DIR__ . '/../..' . '/app/Http/Controllers/Auth/RegisterController.php',
  13. ...)

简单吧,直接命名空间全名与目录的映射,没有顶级命名空间。。。简单粗暴,也导致这个数组相当的大。

PSR0 顶级命名空间映射:

  1. <?php
  2. public static $prefixesPsr0 = array (
  3. 'P' => array (
  4. 'Prophecy\\' => array (
  5. 0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
  6. ),
  7. 'Parsedown' => array (
  8. 0 => __DIR__ . '/..' . '/erusev/parsedown',
  9. ),
  10. ),
  11. 'M' => array (
  12. 'Mockery' => array (
  13. 0 => __DIR__ . '/..' . '/mockery/mockery/library',
  14. ),
  15. ),
  16. 'J' => array (
  17. 'JakubOnderka\\PhpConsoleHighlighter' => array (
  18. 0 => __DIR__ . '/..' . '/jakub-onderka/php-console-highlighter/src',
  19. ),
  20. 'JakubOnderka\\PhpConsoleColor' => array (
  21. 0 => __DIR__ . '/..' . '/jakub-onderka/php-console-color/src',
  22. ),
  23. ),
  24. 'D' => array (
  25. 'Doctrine\\Common\\Inflector\\' => array (
  26. 0 => __DIR__ . '/..' . '/doctrine/inflector/lib',
  27. ),
  28. ),
  29. );

为了快速找到顶级命名空间,我们这里使用命名空间第一个字母作为前缀索引。这个映射的用法比较明显,假如我们有Parsedown/example这样的命名空间,首先通过首字母P,找到

  1. <?php
  2. 'P' => array (
  3. 'Prophecy\\' => array (
  4. 0 => __DIR__ . '/..' . '/phpspec/prophecy/src',
  5. ),
  6. 'Parsedown' => array (
  7. 0 => __DIR__ . '/..' . '/erusev/parsedown',
  8. ),
  9. ),

这个数组,然后我们就会遍历这个数组来和 Parsedown/example 比较,发现第一个 Prophecy 不符合,第二个 Parsedown 符合,然后得到了映射目录:(映射目录可能不止一个)

  1. <?php
  2. array (0 => __DIR__ . '/..' . '/erusev/parsedown',)

我们会接着遍历这个数组,尝试 __DIR__ . '/..' . '/erusev/parsedown/Parsedown/example.php' 是否存在,如果不存在接着遍历数组(这个例子数组只有一个元素),如果数组遍历完都没有,就会加载失败。

PSR4标准顶级命名空间映射数组:

  1. <?php
  2. public static $prefixLengthsPsr4 = array(
  3. 'p' => array (
  4. 'phpDocumentor\\Reflection\\' => 25,
  5. ),
  6. 'S' => array (
  7. 'Symfony\\Polyfill\\Mbstring\\' => 26,
  8. 'Symfony\\Component\\Yaml\\' => 23,
  9. 'Symfony\\Component\\VarDumper\\' => 28,
  10. ...
  11. ),
  12. ...);
  13. public static $prefixDirsPsr4 = array (
  14. 'phpDocumentor\\Reflection\\' => array (
  15. 0 => __DIR__ . '/..' . '/phpdocumentor/reflection-common/src',
  16. 1 => __DIR__ . '/..' . '/phpdocumentor/type-resolver/src',
  17. 2 => __DIR__ . '/..' . '/phpdocumentor/reflection-docblock/src',
  18. ),
  19. 'Symfony\\Polyfill\\Mbstring\\' => array (
  20. 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
  21. ),
  22. 'Symfony\\Component\\Yaml\\' => array (
  23. 0 => __DIR__ . '/..' . '/symfony/yaml',
  24. ),
  25. ...)

PSR4标准顶级命名空间映射用了两个数组,第一个和 PSR0 一样用命名空间第一个字母作为前缀索引,然后是 顶级命名空间,但是最终并不是文件路径,而是 顶级命名空间 的长度。为什么呢?因为前一篇文章我们说过,PSR4标准 的文件目录更加灵活,更加简洁。

PSR0 中顶级命名空间目录直接加到命名空间前面就可以得到路径

  1. Parsedown/example => __DIR__ . '/..' . '/erusev/parsedown/Parsedown/example.php
  2. ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

而 PSR4标准 却是用顶级命名空间目录替换顶级命名空间,所以获得顶级命名空间的长度很重要。

  1. Parsedown/example => __DIR__ . '/..' . '/erusev/parsedown/example.php
  2. ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑

具体的用法:假如我们找 Symfony\\Polyfill\\Mbstring\\example 这个命名空间,和 PSR0 一样通过前缀索引和字符串匹配我们得到了

  1. <?php
  2. 'Symfony\\Polyfill\\Mbstring\\' => 26,

这条记录,键是顶级命名空间,值是命名空间的长度。拿到顶级命名空间后去 $prefixDirsPsr4数组 获取它的映射目录数组:(注意映射目录可能不止一条)

  1. <?php
  2. 'Symfony\\Polyfill\\Mbstring\\' => array (
  3. 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring',
  4. )

然后我们就可以将命名空间 Symfony\\Polyfill\\Mbstring\\example 前26个字符替换成目录 __DIR__ . '/..' . '/symfony/polyfill-mbstring ,我们就得到了__DIR__ . '/..' . '/symfony/polyfill-mbstring/example.php,先验证磁盘上这个文件是否存在,如果不存在接着遍历。如果遍历后没有找到,则加载失败。

自动加载核心类ClassLoader的静态初始化完成!!!

ClassLoader接口初始化


如果PHP版本低于5.6或者使用 HHVM 虚拟机环境,那么就要使用核心类的接口进行初始化。

  1. <?php
  2. //PSR0标准
  3. $map = require __DIR__ . '/autoload_namespaces.php';
  4. foreach ($map as $namespace => $path) {
  5. $loader->set($namespace, $path);
  6. }
  7. //PSR4标准
  8. $map = require __DIR__ . '/autoload_psr4.php';
  9. foreach ($map as $namespace => $path) {
  10. $loader->setPsr4($namespace, $path);
  11. }
  12. $classMap = require __DIR__ . '/autoload_classmap.php';
  13. if ($classMap) {
  14. $loader->addClassMap($classMap);
  15. }

PSR0标准

autoload_namespaces:

  1. <?php
  2. return array(
  3. 'Prophecy\\'
  4. => array($vendorDir . '/phpspec/prophecy/src'),
  5. 'Parsedown'
  6. => array($vendorDir . '/erusev/parsedown'),
  7. 'Mockery'
  8. => array($vendorDir . '/mockery/mockery/library'),
  9. 'JakubOnderka\\PhpConsoleHighlighter'
  10. => array($vendorDir . '/jakub-onderka/php-console-highlighter/src'),
  11. 'JakubOnderka\\PhpConsoleColor'
  12. => array($vendorDir . '/jakub-onderka/php-console-color/src'),
  13. 'Doctrine\\Common\\Inflector\\'
  14. => array($vendorDir . '/doctrine/inflector/lib'),
  15. );

PSR0标准的初始化接口:

  1. <?php
  2. public function set($prefix, $paths)
  3. {
  4. if (!$prefix) {
  5. $this->fallbackDirsPsr0 = (array) $paths;
  6. } else {
  7. $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
  8. }
  9. }

很简单,PSR0标准取出命名空间的第一个字母作为索引,一个索引对应多个顶级命名空间,一个顶级命名空间对应多个目录路径,具体形式可以查看上面我们讲的 autoload_static 的 $prefixesPsr0 。如果没有顶级命名空间,就只存储一个路径名,以便在后面尝试加载。

PSR4标准

autoload_psr4

  1. <?php
  2. return array(
  3. 'XdgBaseDir\\'
  4. => array($vendorDir . '/dnoegel/php-xdg-base-dir/src'),
  5. 'Webmozart\\Assert\\'
  6. => array($vendorDir . '/webmozart/assert/src'),
  7. 'TijsVerkoyen\\CssToInlineStyles\\'
  8. => array($vendorDir . '/tijsverkoyen/css-to-inline-styles/src'),
  9. 'Tests\\'
  10. => array($baseDir . '/tests'),
  11. 'Symfony\\Polyfill\\Mbstring\\'
  12. => array($vendorDir . '/symfony/polyfill-mbstring'),
  13. ...
  14. )

PSR4标准的初始化接口:

  1. <?php
  2. public function setPsr4($prefix, $paths)
  3. {
  4. if (!$prefix) {
  5. $this->fallbackDirsPsr4 = (array) $paths;
  6. } else {
  7. $length = strlen($prefix);
  8. if ('\\' !== $prefix[$length - 1]) {
  9. throw new \InvalidArgumentException(
  10. "A non-empty PSR-4 prefix must end with a namespace separator."
  11. );
  12. }
  13. $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
  14. $this->prefixDirsPsr4[$prefix] = (array) $paths;
  15. }
  16. }

PSR4初始化接口也很简单。如果没有顶级命名空间,就直接保存目录。如果有命名空间的话,要保证顶级命名空间最后是 \ ,然后分别保存

  1. ( 前缀 -> 顶级命名空间,顶级命名空间 -> 顶级命名空间长度 )
  2. ( 顶级命名空间 -> 目录 )

这两个映射数组。具体形式可以查看上面我们讲的 autoload_static 的 prefixLengthsPsr4 、 $prefixDirsPsr4 。

傻瓜式命名空间映射

autoload_classmap:

  1. <?php
  2. public static $classMap = array (
  3. 'App\\Console\\Kernel'
  4. => __DIR__ . '/../..' . '/app/Console/Kernel.php',
  5. 'App\\Exceptions\\Handler'
  6. => __DIR__ . '/../..' . '/app/Exceptions/Handler.php',
  7. ...
  8. )

addClassMap:

  1. <?php
  2. public function addClassMap(array $classMap)
  3. {
  4. if ($this->classMap) {
  5. $this->classMap = array_merge($this->classMap, $classMap);
  6. } else {
  7. $this->classMap = $classMap;
  8. }
  9. }

这个最简单,就是整个命名空间与目录之间的映射。

结语

其实我很想接着写下下去,但是这样会造成篇幅过长,所以我就把自动加载的注册和运行放到下一篇文章了。我们回顾一下,这篇文章主要讲了:

  1. 1.框架如何启动composer自动加载;
  2. 2.composer自动加载分为5部分;

其实说是5部分,真正重要的就两部分——初始化与注册。初始化负责顶层命名空间的目录映射,注册负责实现顶层以下的命名空间映射规则。

Written with StackEdit.